iT邦幫忙

2025 iThome 鐵人賽

DAY 3
1

🎯 今天要做什麼?

昨天我們深入了解了斷言的各種用法,今天要學習 TDD 的精髓 —「紅綠重構循環」。

想像一下,你接到一個需求:「我們需要一個判斷質數的函數。」以前你可能直接開始寫程式,但現在我們要用 TDD 的方式:先寫測試(紅燈),再寫最簡實作(綠燈),最後改善代碼(重構)。

學習目標

今天結束後,你將學會:

  • 深度理解 TDD 的三個階段:紅燈、綠燈、重構
  • 掌握每個階段的具體操作和心態
  • 體驗完整的 TDD 開發節奏
  • 學會基本重構手法

TDD 循環的核心理念

TDD 的核心是一個簡單而強大的三步循環:

🔴 紅燈(Red)   ➜  🟢 綠燈(Green)  ➜  🔵 重構(Refactor)
  ↑                                           ↓
  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←  ←
  • 🔴 紅燈階段:寫失敗的測試,表達期望的行為
  • 🟢 綠燈階段:用最簡單的方法讓測試通過
  • 🔵 重構階段:在測試保護下改善代碼品質

🔴 紅燈階段:寫失敗的測試

紅燈階段的核心思想:先思考需求,再動手寫程式

建立 tests/day03/math-utils.test.ts

import { describe, it, expect } from 'vitest'

describe('math utilities', () => {
  describe('isPrime prime detection', () => {
    it('identifies small prime numbers', () => {
      // 還沒有 isPrime 函數,所以這個測試會失敗(紅燈)
      expect(isPrime(2)).toBe(true)
      expect(isPrime(3)).toBe(true)
      expect(isPrime(5)).toBe(true)
    })

    it('identifies small composite numbers', () => {
      expect(isPrime(4)).toBe(false)
      expect(isPrime(6)).toBe(false)
      expect(isPrime(9)).toBe(false)
    })
  })
})

執行測試:

npm test -- day03

預期結果:測試失敗,因為 isPrime 函數還不存在。這就是我們要的「紅燈」!

🟢 綠燈階段:最快速度讓測試通過

綠燈階段的核心思想:用最簡單的方法讓測試通過

建立 src/math/mathUtils.ts

export function isPrime(n: number): boolean {
  // 最簡單的實作:硬編碼我們測試的數字
  if (n === 2 || n === 3 || n === 5) {
    return true
  }
  if (n === 4 || n === 6 || n === 9) {
    return false
  }
  return false // 其他數字先回傳 false
}

更新測試檔,加入 import:

import { describe, it, expect } from 'vitest'
import { isPrime } from '../../src/math/mathUtils'

// ... 測試內容保持不變

執行測試:

npm test -- day03

結果:測試通過!我們達到了綠燈階段。

🔵 重構階段:改善代碼品質

重構階段的核心思想:在測試保護下,改善代碼品質

我們的硬編碼實作太醜了,讓我們重構:

export function isPrime(n: number): boolean {
  // 處理邊界情況
  if (n < 2) return false
  if (n === 2) return true
  if (n % 2 === 0) return false
  
  // 檢查奇數因子到 sqrt(n)
  for (let i = 3; i * i <= n; i += 2) {
    if (n % i === 0) return false
  }
  
  return true
}

執行測試確認重構成功:

npm test -- day03

測試仍然通過!重構成功。

🚀 完整循環實戰演練

讓我們做第二輪循環,增加邊界情況的測試:

it('handles boundary cases', () => {
  expect(isPrime(0)).toBe(false)
  expect(isPrime(1)).toBe(false)
  expect(isPrime(-1)).toBe(false)
})

it('handles larger prime numbers', () => {
  expect(isPrime(11)).toBe(true)
  expect(isPrime(13)).toBe(true)
})

執行測試 - 全部通過!因為我們的重構實作已經正確處理了這些情況。

TDD 開發節奏

TDD 不只是技術,更是一種開發節奏:

  1. 小步快跑:每次只測試一個小功能
  2. 快速反饋:幾分鐘完成一個循環
  3. 持續驗證:每次修改都有測試保護
  4. 漸進改善:通過重構持續提升品質

心理建設要點

  • 紅燈不可怕:失敗指引方向
  • 綠燈要克制:最簡實作,避免過度工程
  • 重構要勇敢:有測試護航,放心改善

重構時機的判斷

看到這些「代碼異味」就該重構了:

  • 重複代碼:相同邏輯出現多次
  • 過長函數:函數做太多事情
  • 魔術數字:硬編碼的數值
  • 不清楚的命名:變數或函數名不明確

常見的重構手法

1. 提取常數

重構前:

if (age >= 18) { /* ... */ }

重構後:

const MIN_ADULT_AGE = 18
if (age >= MIN_ADULT_AGE) { /* ... */ }

2. 重命名變數

重構前:

function calc(x: number) { return x * 0.1 }

重構後:

function calculateTax(price: number) { 
  const TAX_RATE = 0.1
  return price * TAX_RATE 
}

💡 Vitest 的 TDD 優勢

Vitest 讓 TDD 變得更簡潔、更直觀:

快速熱重載

npm test -- --watch

TypeScript 型別檢查

在重構階段,TypeScript 能幫助我們安全地重構:

export function isPrime(n: number): boolean {  // 明確的型別
  // 改變函數簽名時,TypeScript 會提醒哪些地方需要修改
}

更好的可讀性

與傳統測試框架相比,Vitest 的語法更像在描述需求而非寫程式碼。這讓我們在 TDD 的紅燈階段更容易專注於「需求是什麼」,而不是「怎麼寫測試」。

完整實作範例

完整 src/math/mathUtils.ts

export function isPrime(n: number): boolean {
  if (n < 2) return false
  if (n === 2) return true
  if (n % 2 === 0) return false
  
  for (let i = 3; i * i <= n; i += 2) {
    if (n % i === 0) return false
  }
  
  return true
}

完整 tests/day03/math-utils.test.ts

import { describe, it, expect } from 'vitest'
import { isPrime } from '../../src/math/mathUtils'

describe('math utilities', () => {
  describe('isPrime prime detection', () => {
    it('identifies small prime numbers', () => {
      expect(isPrime(2)).toBe(true)
      expect(isPrime(3)).toBe(true)
      expect(isPrime(5)).toBe(true)
    })

    it('identifies small composite numbers', () => {
      expect(isPrime(4)).toBe(false)
      expect(isPrime(6)).toBe(false)
      expect(isPrime(9)).toBe(false)
    })

    it('handles boundary cases', () => {
      expect(isPrime(0)).toBe(false)
      expect(isPrime(1)).toBe(false)
      expect(isPrime(-1)).toBe(false)
    })

    it('handles larger prime numbers', () => {
      expect(isPrime(11)).toBe(true)
      expect(isPrime(13)).toBe(true)
    })
  })
})

📝 今天學到什麼?

技術收穫

  • 掌握 TDD 三階段:紅燈寫測試、綠燈快速實作、重構改善品質
  • 理解各階段心態:紅燈專注需求、綠燈專注通過、重構專注品質
  • 體驗開發節奏:小步快跑、快速反饋、持續改善
  • 學會基本重構:提取常數、重命名變數

關鍵要點

  • 紅燈是正常的:失敗的測試指引方向
  • 綠燈要克制:最簡實作就好,不要想太複雜
  • 重構很安全:有測試保護,可以放心改善代碼
  • 小步快跑:每次只改一點點,頻繁執行測試

🎉 總結

TDD 的紅綠重構循環看似簡單,但要真正掌握需要大量練習。它不只是技術方法,更是一種思維模式的轉變。

TDD 的紅綠重構循環看似簡單,但要真正掌握需要大量練習。它不只是技術方法,更是一種思維模式的轉變。

今日小挑戰 🏆

試著用 TDD 方式實作一個 isEven 函數:

  1. 先寫測試(什麼數字是偶數?)
  2. 最簡實作(讓測試通過)
  3. 重構改善(讓代碼更優雅)

記住 TDD 的節奏:紅燈 → 綠燈 → 重構,小步快跑!

明天我們將學習「測試結構和組織」,了解如何讓測試更清晰、更好維護。


上一篇
Day 02 - 認識斷言(Assertions) 🚀
下一篇
Day 04 - 測試結構與組織 🚀
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言